Skip to content

feat!: redesign argument and type system as v6 (core + shims, golden-tested)#334

Merged
markdumay merged 23 commits into
mainfrom
feat/args-system-redesign
Jul 12, 2026
Merged

feat!: redesign argument and type system as v6 (core + shims, golden-tested)#334
markdumay merged 23 commits into
mainfrom
feat/args-system-redesign

Conversation

@markdumay

@markdumay markdumay commented Jul 12, 2026

Copy link
Copy Markdown
Collaborator

Important

This is a breaking release: mod-utils v6 (module path github.com/gethinode/mod-utils/v6).
Although the InitArgs/InitTypes API is drop-in compatible, validation behavior changes in
ways that can affect rendered output and build logs (nested and falsy defaults now apply, null
members are dropped from nested maps, all problems reported per call, new warnings surface).
Consumers update the import path in go.mod and hugo.toml; sites using
setup-cloudcannon-cms must also refresh expose globs referencing the vendored v5 path.
See README "Migrating from v5 to v6". Hinode should absorb this in its own next major
(naturally Hinode v2). The warning-to-error promotion is planned as v7.

Summary

Redesigns mod-utils' argument/type validation system (spec:
docs/superpowers/specs/2026-07-12-args-type-system-redesign-design.md,
status now Implemented (phases 0-4)). Two concerns that were previously interleaved in the
270-line InitArgs.html are now separated: a cached schema compiler (ArgsSchema.html) and a
recursive validator (Args.html). InitArgs.html/InitTypes.html become thin compatibility
shims so all ~160 existing call sites keep working unchanged, with newly detectable problems
surfacing as warnings first.

Built with a characterization-first golden-file harness (12 test groups in exampleSite) so
every behavior change — bug fix or otherwise — is visible as an explicit, reviewed diff in its
own commit.

Six defects fixed (design spec §1.1, items 1-6)

  1. Nested defaults used the parent's default — each child now gets its own default.
  2. Nested validation was zero-deep — validation, options/range checks, and unknown-key
    detection now recurse to arbitrary depth (UDT → UDT → scalar, lists of dicts).
  3. Falsy values skipped type validation — explicit false/0/"" are now type-checked.
  4. or-based config fallthrough — an explicit false/0 site parameter is now honored
    instead of silently falling back to the static default.
  5. Return-envelope namespace collisionArgs.html's envelope separates user values
    (args) from bookkeeping (err/errmsg/warnmsg/defaulted); an argument named default
    or err can no longer collide.
  6. First-error-wins — every problem is now collected and reported, not just the first.

Warnings-first strictness rollout

Newly detectable problem classes ship as warnings through the InitArgs.html/InitTypes.html
shims for one full v5 release cycle before promotion to errors:

  • Falsy-value type/select mismatches at the top level
  • Nested member type/select/range findings
  • Unknown nested attributes
  • Excess positional arguments

Args.html called directly with strict: true (default) already treats all four as errors
today.

CloudCannon compatibility preserved

Bookshop golden cases pin CloudCannon's editor-shaped payloads (every blueprint key present,
typically null, plus _bookshop_name/_ordinal, snake_case keys) as err: false at every
nesting level, distinct from genuinely-wrong explicit values, which still validate. No YAML
schema, blueprint, or external call-site changes anywhere in the ecosystem.

Metrics

ArgsSchema.html cumulative time on the Hinode exampleSite build (--templateMetrics), after
enabling partialCached keyed by (structure, bookshop, child):

Before After Speedup
ArgsSchema.html 2m05.18s 0.88s 142x

~84.5k ArgsSchema.html invocations, ~100% cache hits after warmup.

Follow-up (deferred, spec phase 5)

  • Migrate Hinode v2 + first-party module call sites to Args.html directly.
  • args.md docs shortcode consumes ArgsSchema.html instead of re-deriving types.
  • Seed mod-blocks' exampleSite with representative block content (currently a placeholder page,
    no signal for the integration smoke test).
  • Cosmetic cleanup of mod-fontawesome's fas/fab/icon shortcodes (stop forwarding excess
    positions they already handle) before the promotion-to-errors release.
  • Manual CloudCannon live-editing verification on a connected site (release gate, human task).

🤖 Generated with Claude Code

markdumay and others added 20 commits July 12, 2026 06:36
- Inventory of ten confirmed defects and design-debt items in
  InitArgs.html and InitTypes.html, each mapped to a future test
- Agreed decisions: clean core plus compatibility shim inside v5,
  fully recursive validation, golden-file test harness, cached
  schema compilation, camelCase-only canonical keys
- Architecture: ArgsSchema.html compiler (partialCached), recursive
  inline validator, Args.html entry point with separated envelope,
  InitArgs/InitTypes reduced to thin shims
- Phased rollout with characterization-first golden tests and a
  warnings-first strictness transition

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Document the live-editing sandbox expose-glob placement rule for
  new partials and the null-vs-explicit value distinction required
  by CloudCannon-authored content
- Add CloudCannon-shaped golden cases and child-structure coverage
  to the test harness section
- Correct the phase-3 gate: the mod-blocks exampleSite is an empty
  placeholder and provides no integration signal until seeded

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Eleven bite-sized tasks: golden harness, five characterization
  groups, ArgsSchema compiler, Args entry point with recursive
  validator, InitArgs/InitTypes shims, schema caching, docs
- Load-bearing mechanics verified live before planning: JSON output
  template resolution, the dropped data mount in the exampleSite,
  and an end-to-end InitArgs call demonstrating the nested-default
  defect
- Every intentional behavior change is gated by an enumerated
  golden-diff review category

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add data mount to exampleSite/hugo.toml to expose module's data
  files
- Create test-runner layout at exampleSite/layouts/tests/single.json
  to render InitArgs result envelopes as JSON
- Create test fixture files: structure, case group, and content
  page for envelope characterization test
- Add golden comparison script (tests/golden.mjs) for comparing
  generated test output against committed golden files
- Update package.json scripts: add pretest, test, and test:update
  commands
- Generate initial golden baseline that characterizes BUG(5): user
  argument 'default' is clobbered by bookkeeping 'default' slice

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Add test fixtures for static, config-based, and deprecated defaults
- Add test fixtures for type casting and collection types
- Add test fixtures for select validation and numeric ranges
- Capture BUG(3): explicit false skips type validation
- Capture BUG(4): false config value falls through to static default
- Document latent quirks: empty string matches float regex, float64
  type handling

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…avior

- Fix dict-case comments: both plain map and slice-of-maps forms ERROR
  today; the legacy 'dict' alias ([]map[string]interface {}) never
  matches real YAML data
- Document why wrong-type-errors uses a string instead of the composite
  [1, 2]: that form crashes the whole Hugo build (findRE at
  InitArgs.html:154 cannot cast []interface {} to string), so it cannot
  be pinned in a golden
- Goldens unchanged (comment-only edits to the case file)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add test-required.yml fixture with required args, positional args, and
  group-scoped arguments
- Add positional test cases: both-positions, first-position-only,
  excess-position-errors
- Add required test cases: required-provided, required-missing-errors,
  group-filter-skips-partial-arg, group-partial-requires-both,
  no-group-requires-all, first-error-wins
- Pin BUG(6) behavior: first-error-wins reports exactly one errmsg
- Verify group-partial-requires-both correctly errors on missing
  partial-only argument
- Add content pages positional.md and required.md for test group
  generation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ping

- Add nesting group: 7 cases pinning BUG(1) (nested defaults use parent nil)
  and BUG(2) (zero-deep validation of nested members)
- Add child group: 3 cases verifying child-structure merging with cascade
  and merge parent-flag behavior
- Add frontmatter group: 2 cases comparing params-typed nested maps against
  data-typed variants to detect maps.Params type-matching blind spot
- Create shadowing _types.yml in exampleSite with module sync note
- Golden check passed (9 groups); no new latent defects found

Key findings:
  - BUG(1) confirmed: absent-udt-shape-fill shows {"align": null, ...}
    instead of {"align": "start", "width": 8, ...} (member defaults ignored)
  - BUG(2) confirmed: nested-wrong-type-passes allows align: 42 (select type)
  - No maps.Params blind spot: params-typed-nested-map and heading-valid
    produce identical output, both pass validation

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Pin blueprint derivation from Bookshop spec (test-hero.bookshop.yml)
- Verify sidecar merge with kebab-case keys (test-hero.yml)
- Document CloudCannon null-heavy payload handling (err: false preserved)
- Capture snake/kebab normalization with prefer-snake_case warning
- Verify blueprint scalar defaults (link_type, width) survive merge
- Note: blueprint show_more: false not preserved when arg is null (candidate defect)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…se comment

Move the sidecar-merge default-survival observation into the case file, as
required by the task brief: link_type and width defaults are applied for
null keys, show_more's false default is never applied (legacy InitArgs
gates defaults with `or $def.config $def.default`, and false is falsy),
and heading.align stays null. Comment-only change; goldens unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Compile structure/bookshop argument definitions (global _arguments.yml,
_types.yml, structure files, blueprint+sidecar, and parent-flagged child
arguments) into a self-contained recursive schema tree, keyed by
(structure, bookshop, child). Restructures the generated compile-node
inline partial to use a single, unconditional trailing return instead of
an early-return-plus-final-return pair: this Hugo version rejects a
second `return` in the same defined template ("wrong number of args for
return: want 0 got 1"), even when only one is ever reached at runtime;
the restructuring preserves identical semantics (verified against the
`with`-is-falsy behavior of an empty dict).

Adds a `schema` golden group (11th group) exercising scalar structures,
nested UDT recursion to depth 3, child-structure merging, bookshop
blueprint+sidecar merging, and compile-time error paths. All 10
pre-existing golden files are byte-identical; InitArgs.html and
InitTypes.html are untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two accepted design-gap fixes in the ArgsSchema compiler:

- Package-qualified Go reflect-type names declared in _arguments.yml
  (e.g. template.HTML, hstring.HTML) no longer emit 'schema: unknown
  type'; they are collected verbatim on the node as an optional
  'reflects' field ([]string, declared order) for a printf "%T" match
  at validation time, restoring legacy parity. Dot-less unresolvable
  type names still error.
- Child error messages are appended element-wise instead of as whole
  slices, defeating a Hugo append type flip-flop that nested sub-arrays
  into errmsg when zero-error and error-producing UDT members
  interleaved.

nested-structure and bookshop-blueprint now compile err:false with
empty errmsg; compile-errors keeps exactly its two real messages as a
flat array. Only the schema golden changed; the 10 pre-existing goldens
remain byte-identical.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Fixes nested defaults, zero-deep validation, falsy-value skips, and
  or-based config fallthrough on the strict path (spec defects 1-4, 6)
- Adds side-by-side strict results to every golden group

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BREAKING-ISH (warnings only): newly detectable validation findings
(nested type mismatches, unknown nested attributes, falsy-value type
mismatches) surface as warnings; promotion to errors follows one
release cycle later per the design spec.

Golden diffs in this commit are intentional: nested defaults fixed,
all errors reported (no first-error break), explicit-false site
params honored, camelCase duplicates added for snake_case keys,
excess-positional-argument err now reflects errmsg length, and two
pre-existing merge/type-alias bugs are fixed at the ArgsSchema/Args
level (required to unblock the Hinode smoke test, see below).

Also fixes three latent bugs in the shared Args.html/ArgsSchema.html
core, exposed for the first time by routing every InitArgs caller
through it:
- numeric range/min-max checks no longer get the falsy-value
  exemption meant only for type casting (legacy never exempted them
  either; fixes bookshop/cloudcannon-explicit-false staying err:true)
- unknown-type detection now accepts bracket/asterisk Go type names
  ([]string, map[string]interface {}), not just dotted ones, fixing
  a hard build crash in LogErr/LogWarn's own argument validation
- same-named global/local arguments that redeclare "type" no longer
  leak the global's default/config/options via merge's recursive
  nested-map behavior (fixed a hard build error on every Hinode page
  via button.yml's deprecated "size" colliding with the global
  heading "size", and fully resolves envelope/default-arg-collision)

Hinode exampleSite smoke test: build now completes (previously hard
crashed on the first logged warning anywhere). 45 remaining ERRORs
are confined to mod-fontawesome's fas/fab/icon shortcodes, a
downstream-module consequence of the intended err/errmsg semantics
change on excess positional arguments, not a mod-utils defect; needs
a follow-up fix in mod-fontawesome. New WARN lines (~55 distinct
patterns) are the intended warnings-first surface: mostly empty-
string values now flagged where legacy silently ignored them, and
newly-visible nested unsupported-attribute/type findings across
card, image, table, testimonials, hero, and sidebar components.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Coordinator adjudication overruled the planned err-flip on excess
positional arguments: the spec's acceptance criterion requires zero
new errors through the shim path on the Hinode exampleSite, and
warnings-first governs any newly-enforced failure class. Legacy
surfaced the "unsupported argument at index %d" message but never
gated on it (err stayed false, mapped args stayed populated), so
enforcing it now would be a behavioral break.

The positional-mapping loop in Args.html appends the finding to
newmsg instead of errmsg: strict mode promotes it to a real error
for clean-API callers, while the InitArgs shim keeps err false with
args populated and the message in warnmsg — legacy-faithful except
the message slice, resolving legacy's self-contradictory
errmsg-with-err:false shape.

Golden delta is confined to positional/excess-position-errors: the
initargs half reverts to err false with name/kind populated and the
message in warnmsg; the strict args half keeps err true. Hinode
exampleSite smoke test now builds with zero ERROR lines: the 45
mod-fontawesome fas/fab/icon errors are gone (23 became warnings
with unchanged text, the 21 secondary "Missing icon name" failures
vanished since icon args stay populated). The distinct
warning-message set is identical to the previous run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add an inittypes golden group covering scalar, nested-UDT, bookshop-blueprint,
and child-merge structures, exercised through the current legacy InitTypes
implementation before it is rewired as a shim over ArgsSchema.

No fixture crashes the legacy path (the union-typed 'title' field resolves
via Hugo's append-flattening of slice.type into aliases, so no map is
indexed by a slice key). Two legacy quirks are captured verbatim for later
reconciliation: bookshop blueprint dict values (e.g. heading) leak their raw
instance keys into the top-level type def, and CloudCannon 'editors'
metadata leaks through on nested UDT members that declare it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Replace the 156-line legacy layouts/_partials/utilities/InitTypes.html with
the ~50-line shim over ArgsSchema.html, reconstructing the legacy
{types, udt, err, errmsg, warnmsg} contract from schema nodes. types values
use the node's declared type list, collapsing to a plain string when there is
exactly one (matches assets/args.html's `index $types $val.type` lookup);
udt is keyed by top-level UDT type name only, carrying `_reflect` computed
from node.kind (matches legacy's printf "%T" values for dict vs list UDTs).

Every changed inittypes golden line falls into one of four reconciled
categories:
- bookshop blueprint dict values (e.g. heading) no longer leak their raw
  instance keys (align, title) into the top-level type def — a legacy
  quirk from merging the raw blueprint value into the def; assets/args.html
  never reads these extra keys, so this is inert to the verified consumer.
- CloudCannon 'editors' metadata no longer leaks through on nested UDT
  members that declare it, for the same reason (never read by args.html).
- show_more (bookshop-component) gains a "default": false it previously
  lost — this is the sidecar-default fix already characterized in
  ArgsSchema.html (Task 6).
- ratio (child-merge) loses its inherited "options.values" — the child
  structure overrides the global argument's type (select -> string), so
  the type-collision clean-slate merge (Task 8 fix #4) correctly drops the
  now-inapplicable enum constraint.

Verified udt.heading._reflect == "map[string]interface {}" and
udt.locations._reflect == "[]interface {}" in the nested-structure case.

Hinode exampleSite smoke test (HUGO_MODULE_REPLACEMENTS pointing at this
checkout): build succeeds, exit 0, zero ERROR lines. Raw WARN line counts
and per-page attributions are non-deterministic build-cache/parallelism
noise (confirmed by running the unchanged shim twice and observing
different counts/attributions both times); the set of 87 distinct warning
message templates is byte-identical across the pre-shim baseline and two
post-shim runs. No warning or error mentions args.html.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Switched ArgsSchema.html invocations to partialCached in Args.html,
InitArgs.html, and InitTypes.html. Hinode exampleSite --templateMetrics:

  ArgsSchema.html cumulative: 2m5.18s → 0.88s (142x faster)
  Combined (3 partials): 6m25.82s → 1m9.12s (5.58x faster)

Test result: golden check passed (12 groups) — zero behavior drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a README "Argument validation" section covering the Args.html
signature/envelope, the ArgsSchema.html node contract, the InitArgs/
InitTypes shim migration note, the null-vs-explicit tolerance rule,
and the warnings-first classes slated for promotion to errors after
one release cycle.

Mark the design spec Implemented (phases 0-4) and record the
deviation log discovered during execution: the single-return-per-
define restriction, element-wise message appends, reflect-type
passthrough, the untestable cycle fixture, the excess-positional
adjudication, the four core fixes from the Hinode smoke test, and
the two legacy-behavior corrections (dict alias, false/0 defaults).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bumps the Go module path to /v6 and documents the v5-to-v6
migration (import paths, CloudCannon expose globs, behavior
changes). The warning-to-error promotion is now planned as v7.

BREAKING CHANGE: although the InitArgs/InitTypes API is drop-in
compatible, validation behavior changes: nested and falsy defaults
now apply (rendered output can change), null members are dropped
from nested maps, all validation problems are reported per call,
explicit falsy site parameters override static defaults, and newly
detectable problem classes surface as warnings. Consumers must
update the import path to github.com/gethinode/mod-utils/v6 and,
when using setup-cloudcannon-cms, refresh expose globs that
reference the vendored v5 path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@markdumay markdumay changed the title feat: redesign argument and type system (core + shims, golden-tested) feat!: redesign argument and type system as v6 (core + shims, golden-tested) Jul 12, 2026
markdumay and others added 3 commits July 12, 2026 11:36
Windows CI checks out the golden files with CRLF conversion while
Hugo emits LF, so byte comparison failed on windows-latest only.
Pin tests/golden/*.json to LF via .gitattributes and normalize
line endings in golden.mjs as a defensive second layer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@markdumay markdumay merged commit 7410f06 into main Jul 12, 2026
11 checks passed
@markdumay markdumay deleted the feat/args-system-redesign branch July 12, 2026 09:43
@markdumay

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 6.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant